package com.magenta.maxoptra.dto;

/**
 * Created by IntelliJ IDEA.
 * User: antonn
 * Date: 28-May-2010
 * Time: 16:38:14
 * Project: Maxoptra SM
 * <p/>
 * This is a partial port of js library of working with LatLonPoints
 *
 * @ see http://www.movable-type.co.uk/scripts/latlong.html
 */
public class LatLonPoint {
    public static double EARTH_RADIUS = 6371 * 1000;   // earth's mean radius in meters

    private Coordinate lat;
    private Coordinate lon;

    /**
     * @param lat - latitude of point (in deg)
     * @param lon - longitude of point (in deg)
     */
    public LatLonPoint(double lat, double lon) {
        this.lat = new Coordinate(lat);
        this.lon = new Coordinate(lon);
    }

    /**
     * @param lat
     * @param lon
     */
    public LatLonPoint(Coordinate lat, Coordinate lon) {
        this.lat = lat;
        this.lon = lon;
    }

    /**
     * Returns the (initial) bearing from this point to the given point, in degrees
     * see http://williams.best.vwh.net/avform.htm#Crs
     *
     * @param point LatLonPoint
     * @return double Initial bearing in degrees from North
     */
    public double bearingTo(LatLonPoint point) {
        double lat1 = this.lat.toRad();
        double lat2 = point.getLatRad();
        double deltaLon = new Coordinate(point.getLon() - this.getLon()).toRad();

        double y = Math.sin(deltaLon) * Math.cos(lat2);
        double x = Math.cos(lat1) * Math.sin(lat2) - Math.sin(lat1) * Math.cos(deltaLon);

        double bearing = new Coordinate(Math.atan2(y, x), true).toDeg() + 360;

        return bearing % 360;
    }

    /**
     * Returns the destination point from this point having travelled the given distance (in meters) on the
     * given initial bearing (bearing may vary before destination is reached)
     *
     * @param distanceInMeters - distance to point we search for
     * @param bearingInDegrees - bearing to point we search for
     *                         strictly North = 0d
     *                         North-East = 45d
     *                         strictly East = 90d
     *                         South-East = 135d
     *                         strictly South = 180d
     *                         South-West = 225d
     *                         strictly West = 270d
     *                         North-West = 315d
     * @return LatLonPoint
     */
    public LatLonPoint getPointInDistance(double distanceInMeters, double bearingInDegrees) {
        double angularDistance = distanceInMeters / EARTH_RADIUS;
        double bearingInRad = new Coordinate(bearingInDegrees).toRad();

        double lat1 = this.getLatRad();
        double lon1 = this.getLonRad();

        double lat2 = Math.asin(Math.sin(lat1) * Math.cos(angularDistance) + Math.cos(lat1) * Math.sin(angularDistance) * Math.cos(bearingInRad));
        double lon2 = lon1 + Math.atan2(Math.sin(bearingInRad) * Math.sin(angularDistance) * Math.cos(lat1),
                Math.cos(angularDistance) - Math.sin(lat1) * Math.sin(lat2));

        lon2 = (lon2 + 3 * Math.PI) % (2 * Math.PI) - Math.PI;      // normalise to -180...+180

        return new LatLonPoint(new Coordinate(lat2, true), new Coordinate(lon2, true));
    }

    /**
     * Returns the distance from this point to the supplied point, in meters
     * (using Haversine formula)
     * <p/>
     * from: Haversine formula - R. W. Sinnott, "Virtues of the Haversine",
     * Sky and Telescope, vol 68, no 2, 1984
     *
     * @param point LatLonPoint
     * @return double
     */
    public double distanceTo(LatLonPoint point) {
        double lat1 = this.getLatRad();
        double lon1 = this.getLonRad();
        double lat2 = point.getLatRad();
        double lon2 = point.getLonRad();

        double deltaLat = lat2 - lat1;
        double deltaLon = lon2 - lon1;

        double a = Math.pow(Math.sin(deltaLat / 2), 2d) + Math.cos(lat1) * Math.cos(lat2) * Math.pow(Math.sin(deltaLon / 2), 2d);
        double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
        return EARTH_RADIUS * c;
    }

    public double getLat() {
        return lat.toDeg();
    }

    public double getLon() {
        return lon.toDeg();
    }

    public double getLatRad() {
        return lat.toRad();
    }

    public double getLonRad() {
        return lon.toRad();
    }
}
